// 版权归 Epic Games, Inc. 所有。保留所有权利。

#include "LyraFrontendStateComponent.h"

#include "CommonGameInstance.h"
#include "CommonSessionSubsystem.h"
#include "CommonUserSubsystem.h"
#include "ControlFlowManager.h"
#include "GameModes/LyraExperienceManagerComponent.h"
#include "Kismet/GameplayStatics.h"
#include "NativeGameplayTags.h"
#include "PrimaryGameLayout.h"
#include "Widgets/CommonActivatableWidgetContainer.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(LyraFrontendStateComponent)

namespace FrontendTags
{
	UE_DEFINE_GAMEPLAY_TAG_STATIC(TAG_PLATFORM_TRAIT_SINGLEONLINEUSER, "Platform.Trait.SingleOnlineUser"); // 定义平台特性：单在线用户标签
	UE_DEFINE_GAMEPLAY_TAG_STATIC(TAG_UI_LAYER_MENU, "UI.Layer.Menu"); // 定义 UI 层：菜单标签
}

/**
 * 构造函数
 * @param ObjectInitializer 对象初始化器
 */
ULyraFrontendStateComponent::ULyraFrontendStateComponent(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
}

/**
 * 当组件开始播放时调用
 */
void ULyraFrontendStateComponent::BeginPlay()
{
	Super::BeginPlay(); // 调用父类 BeginPlay

	// 监听经验加载完成事件
	AGameStateBase* GameState = GetGameStateChecked<AGameStateBase>(); // 获取游戏状态
	ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass<ULyraExperienceManagerComponent>(); // 查找经验管理器组件
	check(ExperienceComponent); // 确保经验组件有效

	// 此委托位于与此组件生命周期相同的组件上，因此无需在 EndPlay 中取消挂钩
	ExperienceComponent->CallOrRegister_OnExperienceLoaded_HighPriority(FOnLyraExperienceLoaded::FDelegate::CreateUObject(this, &ThisClass::OnExperienceLoaded)); // 注册高优先级经验加载委托
}

/**
 * 当组件结束播放时调用
 * @param EndPlayReason 结束播放的原因
 */
void ULyraFrontendStateComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason); // 调用父类 EndPlay
}

/**
 * 判断是否应显示加载屏幕
 * @param OutReason 输出原因字符串
 * @return 如果需要显示加载屏幕则返回 true，否则返回 false
 */
bool ULyraFrontendStateComponent::ShouldShowLoadingScreen(FString& OutReason) const
{
	if (bShouldShowLoadingScreen) // 如果应显示加载屏幕
	{
		OutReason = TEXT("前端流程待处理..."); // 设置原因字符串
		
		if (FrontEndFlow.IsValid()) // 如果前端流程有效
		{
			const TOptional<FString> StepDebugName = FrontEndFlow->GetCurrentStepDebugName(); // 获取当前步骤调试名称
			if (StepDebugName.IsSet()) // 如果调试名称已设置
			{
				OutReason = StepDebugName.GetValue(); // 使用调试名称作为原因
			}
		}
		
		return true; // 返回需要显示加载屏幕
	}

	return false; // 返回不需要显示加载屏幕
}

/**
 * 当经验加载完成时调用
 * @param Experience 加载的经验定义
 */
void ULyraFrontendStateComponent::OnExperienceLoaded(const ULyraExperienceDefinition* Experience)
{
	FControlFlow& Flow = FControlFlowStatics::Create(this, TEXT("FrontendFlow")) // 创建名为 "FrontendFlow" 的控制流
		.QueueStep(TEXT("等待用户初始化"), this, &ThisClass::FlowStep_WaitForUserInitialization) // 排队步骤：等待用户初始化
		.QueueStep(TEXT("尝试显示 Press Start 屏幕"), this, &ThisClass::FlowStep_TryShowPressStartScreen) // 排队步骤：尝试显示 Press Start 屏幕
		.QueueStep(TEXT("尝试加入请求的会话"), this, &ThisClass::FlowStep_TryJoinRequestedSession) // 排队步骤：尝试加入请求的会话
		.QueueStep(TEXT("尝试显示主屏幕"), this, &ThisClass::FlowStep_TryShowMainScreen); // 排队步骤：尝试显示主屏幕

	Flow.ExecuteFlow(); // 执行流程

	FrontEndFlow = Flow.AsShared(); // 将流程转换为共享指针并存储
}

/**
 * 流程步骤：等待用户初始化
 * @param SubFlow 子流程节点引用
 */
void ULyraFrontendStateComponent::FlowStep_WaitForUserInitialization(FControlFlowNodeRef SubFlow)
{
	// 如果这是硬断开连接，则显式销毁所有用户和会话状态
	// TODO: 重构引擎断开连接流程，使其更明确地说明发生的原因
	bool bWasHardDisconnect = false; // 标记是否为硬断开连接
	AGameModeBase* GameMode = GetWorld()->GetAuthGameMode<AGameModeBase>(); // 获取游戏模式
	UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this); // 获取游戏实例

	if (ensure(GameMode) && UGameplayStatics::HasOption(GameMode->OptionsString, TEXT("closed"))) // 确保游戏模式有效并检查选项字符串中是否有 "closed"
	{
		bWasHardDisconnect = true; // 标记为硬断开连接
	}

	// 仅在硬断开连接时重置用户
	UCommonUserSubsystem* UserSubsystem = GameInstance->GetSubsystem<UCommonUserSubsystem>(); // 获取用户子系统
	if (ensure(UserSubsystem) && bWasHardDisconnect) // 确保用户子系统有效且是硬断开连接
	{
		UserSubsystem->ResetUserState(); // 重置用户状态
	}

	// 总是重置会话
	UCommonSessionSubsystem* SessionSubsystem = GameInstance->GetSubsystem<UCommonSessionSubsystem>(); // 获取会话子系统
	if (ensure(SessionSubsystem)) // 确保会话子系统有效
	{
		SessionSubsystem->CleanUpSessions(); // 清理会话
	}

	SubFlow->ContinueFlow(); // 继续流程
}

/**
 * 流程步骤：尝试显示 Press Start 屏幕
 * @param SubFlow 子流程节点引用
 */
void ULyraFrontendStateComponent::FlowStep_TryShowPressStartScreen(FControlFlowNodeRef SubFlow)
{
	const UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this); // 获取游戏实例
	UCommonUserSubsystem* UserSubsystem = GameInstance->GetSubsystem<UCommonUserSubsystem>(); // 获取用户子系统

	// 检查第一个玩家是否已经登录，如果是，我们可以跳过 Press Start 屏幕。
	if (const UCommonUserInfo* FirstUser = UserSubsystem->GetUserInfoForLocalPlayerIndex(0)) // 获取本地玩家索引 0 的用户信息
	{
		if (FirstUser->InitializationState == ECommonUserInitializationState::LoggedInLocalOnly || // 如果初始化状态为本地登录
			FirstUser->InitializationState == ECommonUserInitializationState::LoggedInOnline) // 或在线登录
		{
			SubFlow->ContinueFlow(); // 继续流程
			return; // 返回
		}
	}

	// 检查平台是否确实需要 'Press Start' 屏幕。这仅在可以有多个在线用户的平台上需要，
	// 具体取决于哪个玩家的控制器按下 'Start' 来确定实际登录游戏的玩家。
	if (!UserSubsystem->ShouldWaitForStartInput()) // 如果不需要等待开始输入
	{
		// 启动自动登录过程，这应该很快完成，并将使用默认输入设备 ID
		InProgressPressStartScreen = SubFlow; // 设置正在进行的 Press Start 屏幕任务
		UserSubsystem->OnUserInitializeComplete.AddDynamic(this, &ULyraFrontendStateComponent::OnUserInitialized); // 添加用户初始化完成动态委托
		UserSubsystem->TryToInitializeForLocalPlay(0, FInputDeviceId(), false); // 尝试为本地播放初始化用户

		return; // 返回
	}

	// 添加 Press Start 屏幕，当其取消激活时移动到下一个流程。
	if (UPrimaryGameLayout* RootLayout = UPrimaryGameLayout::GetPrimaryGameLayoutForPrimaryPlayer(this)) // 获取主玩家的主游戏布局
	{
		constexpr bool bSuspendInputUntilComplete = true; // 定义是否在完成前暂停输入
		RootLayout->PushWidgetToLayerStackAsync<UCommonActivatableWidget>(FrontendTags::TAG_UI_LAYER_MENU, bSuspendInputUntilComplete, PressStartScreenClass, // 异步将小部件推送到层栈
			[this, SubFlow](EAsyncWidgetLayerState State, UCommonActivatableWidget* Screen) { // 定义异步回调 Lambda
			switch (State) // 根据状态切换
			{
			case EAsyncWidgetLayerState::AfterPush: // 推送后状态
				bShouldShowLoadingScreen = false; // 设置不应显示加载屏幕
				Screen->OnDeactivated().AddWeakLambda(this, [this, SubFlow]() { // 添加屏幕取消激活弱 Lambda 委托
					SubFlow->ContinueFlow(); // 继续流程
				});
				break; // 跳出 switch
			case EAsyncWidgetLayerState::Canceled: // 取消状态
				bShouldShowLoadingScreen = false; // 设置不应显示加载屏幕
				SubFlow->ContinueFlow(); // 继续流程
				return; // 返回
			}
		});
	}
}

/**
 * 当用户初始化完成时调用
 * @param UserInfo 用户信息
 * @param bSuccess 是否成功
 * @param Error 错误信息
 * @param RequestedPrivilege 请求的权限
 * @param OnlineContext 在线上下文
 */
void ULyraFrontendStateComponent::OnUserInitialized(const UCommonUserInfo* UserInfo, bool bSuccess, FText Error, ECommonUserPrivilege RequestedPrivilege, ECommonUserOnlineContext OnlineContext)
{
	FControlFlowNodePtr FlowToContinue = InProgressPressStartScreen; // 获取要继续的流程节点指针
	UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this); // 获取游戏实例
	UCommonUserSubsystem* UserSubsystem = GameInstance->GetSubsystem<UCommonUserSubsystem>(); // 获取用户子系统

	if (ensure(FlowToContinue.IsValid() && UserSubsystem)) // 确保流程节点有效且用户子系统有效
	{
		UserSubsystem->OnUserInitializeComplete.RemoveDynamic(this, &ULyraFrontendStateComponent::OnUserInitialized); // 移除用户初始化完成动态委托
		InProgressPressStartScreen.Reset(); // 重置正在进行的 Press Start 屏幕任务

		if (bSuccess) // 如果成功
		{
			// 成功时正常继续流程
			FlowToContinue->ContinueFlow(); // 继续流程
		}
		else // 如果失败
		{
			// TODO: 暂时继续，可以转到某种错误屏幕
			FlowToContinue->ContinueFlow(); // 继续流程
		}
	}
}

/**
 * 流程步骤：尝试加入请求的会话
 * @param SubFlow 子流程节点引用
 */
void ULyraFrontendStateComponent::FlowStep_TryJoinRequestedSession(FControlFlowNodeRef SubFlow)
{
	UCommonGameInstance* GameInstance = Cast<UCommonGameInstance>(UGameplayStatics::GetGameInstance(this)); // 获取通用游戏实例
	if (GameInstance->GetRequestedSession() != nullptr && GameInstance->CanJoinRequestedSession()) // 如果存在请求的会话且可以加入
	{
		UCommonSessionSubsystem* SessionSubsystem = GameInstance->GetSubsystem<UCommonSessionSubsystem>(); // 获取会话子系统
		if (ensure(SessionSubsystem)) // 确保会话子系统有效
		{
			// 绑定会话加入完成事件以继续或取消流程
			// TODO: 需要确保会话加入完成后，服务器旅行完成。
			OnJoinSessionCompleteEventHandle = SessionSubsystem->OnJoinSessionCompleteEvent.AddWeakLambda(this, [this, SubFlow, SessionSubsystem](const FOnlineResultInformation& Result) // 添加加入会话完成弱 Lambda 委托
			{
				// 取消绑定委托。SessionSubsystem 是触发此事件的对象，因此它必须仍然有效。
				SessionSubsystem->OnJoinSessionCompleteEvent.Remove(OnJoinSessionCompleteEventHandle); // 移除加入会话完成事件委托
				OnJoinSessionCompleteEventHandle.Reset(); // 重置委托句柄

				if (Result.bWasSuccessful) // 如果成功
				{
					// 不再转换到主菜单
					SubFlow->CancelFlow(); // 取消流程
				}
				else // 如果失败
				{
					// 继续进行到主菜单
					SubFlow->ContinueFlow(); // 继续流程
					return; // 返回
				}
			});
			GameInstance->JoinRequestedSession(); // 加入请求的会话
			return; // 返回
		}
	}
	// 如果我们没有开始请求加入会话，则跳过此步骤
	SubFlow->ContinueFlow(); // 继续流程
}

/**
 * 流程步骤：尝试显示主屏幕
 * @param SubFlow 子流程节点引用
 */
void ULyraFrontendStateComponent::FlowStep_TryShowMainScreen(FControlFlowNodeRef SubFlow)
{
	if (UPrimaryGameLayout* RootLayout = UPrimaryGameLayout::GetPrimaryGameLayoutForPrimaryPlayer(this)) // 获取主玩家的主游戏布局
	{
		constexpr bool bSuspendInputUntilComplete = true; // 定义是否在完成前暂停输入
		RootLayout->PushWidgetToLayerStackAsync<UCommonActivatableWidget>(FrontendTags::TAG_UI_LAYER_MENU, bSuspendInputUntilComplete, MainScreenClass, // 异步将小部件推送到层栈
			[this, SubFlow](EAsyncWidgetLayerState State, UCommonActivatableWidget* Screen) { // 定义异步回调 Lambda
			switch (State) // 根据状态切换
			{
			case EAsyncWidgetLayerState::AfterPush: // 推送后状态
				bShouldShowLoadingScreen = false; // 设置不应显示加载屏幕
				SubFlow->ContinueFlow(); // 继续流程
				return; // 返回
			case EAsyncWidgetLayerState::Canceled: // 取消状态
				bShouldShowLoadingScreen = false; // 设置不应显示加载屏幕
				SubFlow->ContinueFlow(); // 继续流程
				return; // 返回
			}
		});
	}
}